/**
 * Copyright (c) 2024 Huawei Technologies Co., Ltd.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree.
 */

import { Autolinking, AutolinkingConfig } from './Autolinking';
import { NestedDirectoryJSON } from 'memfs';
import { AbsolutePath, DescriptiveError } from '../core';
import { MockedLogger, MemFS } from '../io/__fixtures__';

function createAutolinking({
  fsStructure,
}: {
  fsStructure: NestedDirectoryJSON;
}) {
  const memFS = new MemFS(fsStructure);
  const mockedLogger = new MockedLogger();
  const autolinking = new Autolinking(memFS, mockedLogger);

  return {
    runAutolinking: async (config: Partial<AutolinkingConfig> = {}) => {
      const input = await autolinking.prepareInput({
        harmonyProjectPath:
          config.harmonyProjectPath ?? new AbsolutePath('./harmony'),
        nodeModulesPath:
          config.nodeModulesPath ?? new AbsolutePath('./node_modules'),
        cppRNOHPackagesFactoryPathRelativeToHarmony:
          config.cppRNOHPackagesFactoryPathRelativeToHarmony ??
          './entry/src/main/cpp/RNOHPackageFactory.h',
        ohPackagePathRelativeToHarmony:
          config.ohPackagePathRelativeToHarmony ?? './oh-package.json5',
        etsRNOHPackagesFactoryPathRelativeToHarmony:
          config.etsRNOHPackagesFactoryPathRelativeToHarmony ??
          './entry/src/main/ets/RNOHPackageFactory.ets',
        cmakeAutolinkPathRelativeToHarmony:
          config.cmakeAutolinkPathRelativeToHarmony ??
          './entry/src/main/cpp/autolink.cmake',
        excludedNpmPackageNames: config.excludedNpmPackageNames ?? new Set(),
        includedNpmPackageNames: config.includedNpmPackageNames ?? new Set(),
      });
      const output = autolinking.evaluate(input);
      autolinking.saveAndLogOutput(output);
      return {
        cmakeAutolinkingPath: output.cmakeAutolinkingPathAndContent[0],
        cppRNOHPackagesFactoryPath:
          output.cppRNOHPackagesFactoryPathAndContent[0],
        etsRNOHPackagesFactoryPath:
          output.etsRNOHPackagesFactoryPathAndContent[0],
        ohPackagePath: output.ohPackagePathAndContent[0],
        logs: mockedLogger.getLogs(),
      };
    },
    memFS,
  };
}

const baseFileStructure = {
  harmony: {
    entry: {
      src: {
        main: {
          ets: {},
          cpp: {},
        },
      },
    },
    'oh-package.json5': `{
      "dependencies": {
      }
    }`,
  },
};

it('should generate correct templates with scoped package default configuration', async () => {
  const { runAutolinking, memFS } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        '@rnoh': {
          'link-scoped': {
            harmony: {
              'link_scoped.har': '',
            },
            'package.json': JSON.stringify({
              name: '@rnoh/link-scoped',
              harmony: {
                autolinking: true,
              },
            }),
          },
        },
      },
    },
  });

  await runAutolinking();

  expect(
    memFS.readTextSync(new AbsolutePath('./harmony/oh-package.json5'))
  ).toBe(
    `
{
  dependencies: {
    "@rnoh/rnoh--link-scoped": "file:../node_modules/@rnoh/link-scoped/harmony/link_scoped.har",
  },
}
`.trimStart()
  );
  expect(
    memFS.readTextSync(
      new AbsolutePath('./harmony/entry/src/main/ets/RNOHPackageFactory.ets')
    )
  ).toBe(
    `
/*
 * This file was generated by RNOH autolinking.
 * DO NOT modify it manually, your changes WILL be overwritten.
 */
import type { RNPackageContext, RNOHPackage } from '@rnoh/react-native-openharmony';
import RnohLinkScopedPackage from '@rnoh/rnoh--link-scoped';

export function createRNOHPackages(ctx: RNPackageContext): RNOHPackage[] {
  return [
    new RnohLinkScopedPackage(ctx),
  ];
}
`.trimStart()
  );

  expect(
    memFS.readTextSync(
      new AbsolutePath('./harmony/entry/src/main/cpp/RNOHPackageFactory.h')
    )
  ).toBe(
    `
/*
 * This file was generated by RNOH autolinking.
 * DO NOT modify it manually, your changes WILL be overwritten.
 */
// clang-format off
#pragma once
#include "RNOH/Package.h"
#include "RnohLinkScopedPackage.h"

std::vector<rnoh::Package::Shared> createRNOHPackages(const rnoh::Package::Context &ctx) {
  return {
    std::make_shared<rnoh::RnohLinkScopedPackage>(ctx),
  };
}
`.trimStart()
  );

  expect(
    memFS.readTextSync(
      new AbsolutePath('./harmony/entry/src/main/cpp/autolink.cmake')
    )
  ).toBe(
    `
# This file was generated by RNOH autolinking.
# DO NOT modify it manually, your changes WILL be overwritten.
cmake_minimum_required(VERSION 3.5)

# @api
function(autolink_libraries target)
    add_subdirectory("\${OH_MODULES_DIR}/@rnoh/rnoh--link-scoped/src/main/cpp" ./rnoh__rnoh__link_scoped)

    set(AUTOLINKED_LIBRARIES
        rnoh__rnoh__link_scoped
    )

    foreach(lib \${AUTOLINKED_LIBRARIES})
        target_link_libraries(\${target} PUBLIC \${lib})
    endforeach()
endfunction()
`.trimStart()
  );
});

it('should handle custom autolinking configuration', async () => {
  const { runAutolinking, memFS } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        '@rnoh': {
          'custom-config': {
            harmony: {
              'custom_config.har': '',
            },
            'package.json': JSON.stringify({
              name: '@rnoh/custom-config',
              harmony: {
                autolinking: {
                  ohPackageName: '@rnoh/custom-oh-name',
                  etsPackageClassName: 'CustomEtsClass',
                  cppPackageClassName: 'CustomCppClass',
                  cmakeLibraryTargetName: 'custom_cmake_target',
                },
              },
            }),
          },
        },
      },
    },
  });

  const output = await runAutolinking();

  expect(memFS.readTextSync(output.etsRNOHPackagesFactoryPath)).toContain(
    'CustomEtsClass'
  );
  expect(memFS.readTextSync(output.cppRNOHPackagesFactoryPath)).toContain(
    'CustomCppClass'
  );
  expect(memFS.readTextSync(output.cmakeAutolinkingPath)).toContain(
    'custom_cmake_target'
  );
  expect(memFS.readTextSync(output.ohPackagePath)).toContain(
    '@rnoh/custom-oh-name'
  );
});

it('should handle unscoped package correctly', async () => {
  const { runAutolinking, memFS } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'unscoped-package': {
          harmony: {
            'unscoped_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'unscoped-package',
            harmony: {
              autolinking: true,
            },
          }),
        },
      },
    },
  });

  const output = await runAutolinking();

  expect(memFS.readTextSync(output.etsRNOHPackagesFactoryPath)).toContain(
    'UnscopedPackage'
  );
  expect(memFS.readTextSync(output.cppRNOHPackagesFactoryPath)).toContain(
    'UnscopedPackage'
  );
  expect(memFS.readTextSync(output.ohPackagePath)).toContain(
    '@rnoh/unscoped-package'
  );
});

it('should remove entries to missing hars from oh-package.json5 and preserve custom entries not managed by autolinking', async () => {
  const { runAutolinking, memFS } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        '@rnoh': {
          'existing-har': {
            harmony: {
              'existing_har.har': '',
            },
            'package.json': JSON.stringify({
              name: '@rnoh/existing-har',
              harmony: {
                autolinking: null,
              },
            }),
          },
          'missing-har': {
            harmony: {
              // No .har file here
            },
            'package.json': JSON.stringify({
              name: '@rnoh/missing-har',
            }),
          },
        },
      },
      harmony: {
        ...baseFileStructure.harmony,
        'oh-package.json5': `{
            "dependencies": {
              "@rnoh/existing-har": "file:../node_modules/@rnoh/existing-har/harmony/existing_har.har",
              "@rnoh/missing-har": "file:../node_modules/@rnoh/missing-har/harmony/missing_har.har"
            }
          }`,
      },
    },
  });

  const output = await runAutolinking();

  const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
  expect(ohPackageContent).toContain('@rnoh/existing-har');
  expect(ohPackageContent).not.toContain('@rnoh/missing-har');
});

it('should log updated files', async () => {
  const { runAutolinking } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'unscoped-package': {
          harmony: {
            'unscoped_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'unscoped-package',
            harmony: {
              autolinking: true,
            },
          }),
        },
      },
    },
  });

  const { logs } = await runAutolinking();

  const combinedLogs = logs.map((log) => log.msg).join('\n');
  expect(combinedLogs).toContain('harmony/entry/src/main/cpp/autolink.cmake');
  expect(combinedLogs).toContain(
    'harmony/entry/src/main/ets/RNOHPackageFactory.ets'
  );
  expect(combinedLogs).toContain(
    'harmony/entry/src/main/cpp/RNOHPackageFactory.h'
  );
  expect(combinedLogs).toContain('harmony/oh-package.json5');
});

it('should log linked and skipped packages', async () => {
  const { runAutolinking } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'autolinkable-package': {
          harmony: {
            'autolinkable_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'autolinkable-package',
            harmony: {
              autolinking: true,
            },
          }),
        },
        'ignored-package': {
          harmony: {
            'ignored_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'ignored-package',
            harmony: {
              autolinking: true,
            },
          }),
        },
      },
    },
  });

  const { logs } = await runAutolinking({
    excludedNpmPackageNames: new Set<string>().add('ignored-package'),
  });

  const combinedLogs = logs.map((log) => log.msg).join('\n');
  expect(combinedLogs).toContain('[link] autolinkable-package');
  expect(combinedLogs).toContain('[skip] ignored-package');
});

it('should fail when user lists included and excluded packages', async () => {
  const { runAutolinking } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'autolinkable-package': {
          harmony: {
            'autolinkable_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'autolinkable-package',
          }),
        },
        'ignored-package': {
          harmony: {
            'ignored_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'ignored-package',
          }),
        },
      },
    },
  });

  expect(() =>
    runAutolinking({
      includedNpmPackageNames: new Set<string>().add('autolinkable-package'),
      excludedNpmPackageNames: new Set<string>().add('autolinkable-package'),
    })
  ).rejects.toThrow(DescriptiveError);
});

it('should link only specified packages', async () => {
  const { runAutolinking } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'autolinkable-package': {
          harmony: {
            'autolinkable_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'autolinkable-package',
            harmony: {
              autolinking: true,
            },
          }),
        },
        'ignored-package': {
          harmony: {
            'ignored_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'ignored-package',
            harmony: {
              autolinking: true,
            },
          }),
        },
      },
    },
  });

  const { logs } = await runAutolinking({
    includedNpmPackageNames: new Set<string>().add('autolinkable-package'),
  });

  const combinedLogs = logs.map((log) => log.msg).join('\n');
  expect(combinedLogs).toContain('[link] autolinkable-package');
  expect(combinedLogs).toContain('[skip] ignored-package');
});


it('should link by default only those packages that support autolinking', async () => {
  const { runAutolinking } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'autolinkable-package': {
          harmony: {
            'autolinkable_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'autolinkable-package',
            harmony: {
              autolinking: true,
            },
          }),
        },
        'not-autolinkable-package': {
          harmony: {
            'not_autolinkable_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'not-autolinkable-package',
          }),
        },
      },
    },
  });

  const { logs } = await runAutolinking({});

  const combinedLogs = logs.map((log) => log.msg).join('\n');
  expect(combinedLogs).toContain('[link] autolinkable-package');
  expect(combinedLogs).toContain('[skip] not-autolinkable-package');
});